using System;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using DZTC.UselessAppEver.Base32.Exceptions;
using DZTC.UselessAppEver.Core;

namespace DZTC.UselessAppEver.Base32
{
    internal class Decoder : Base32Converter
    {
        private enum DecodingState
        {
            ConsumingDigits,
            ConsumingPads
        }

        private static long GetChunkCount(long length)
        {
            var r = length%BASE32_BUFFER_LENGTH;
            return length/BASE32_BUFFER_LENGTH + (r != 0 || length == 0 ? 1 : 0); // same workaround here
        }

        // this implementation is inspired by http://golang.org/src/pkg/encoding/base32/base32.go
        
        // GO version is not fully RFC 4648 compliant
        // and contains too much COPROCODE

        // well, this is not _way_ better :)

        private static unsafe int CommitQuantum(byte* pDBuf, int dlen, byte* pDst)
        {
            // Pack 8x 5-bit source blocks into 5 byte destination
            // quantum

            switch (dlen)
            {
                case 8:
                case 7:
                    pDst[4] = (byte) (pDBuf[6] << 5 | pDBuf[7]);
                    goto case 6; // fallthrough
                case 6:
                case 5:
                    pDst[3] = (byte) (pDBuf[4] << 7 | pDBuf[5] << 2 | pDBuf[6] >> 3);
                    goto case 4; // fallthrough
                case 4:
                    pDst[2] = (byte) (pDBuf[3] << 4 | pDBuf[4] >> 1);
                    goto case 3; // fallthrough
                case 3:
                    pDst[1] = (byte) (pDBuf[1] << 6 | pDBuf[2] << 1 | pDBuf[3] >> 4);
                    goto case 2; // fallthrough

                case 2:
                    pDst[0] = (byte) (pDBuf[0] << 3 | pDBuf[1] >> 2);
                    break;
            }


            // decoding length 
            switch (dlen)
            {
                case 2:
                    return 1;

                case 3:
                case 4:
                    return 2;


                case 5:
                    return 3;

                case 6:
                case 7:
                    return 4;

                case 8:
                    return 5;
            }

            // to keep compiller happy
            throw new NotSupportedException();
        }

        
        // TODO: refactor. introduce some struct/class for keeping decoding state
        
        internal static unsafe void DecodeInternal(Stream input, Stream output, CancellationToken ct, ProgressToken pt)
        {
            var sourceChunk = new byte[BASE32_BUFFER_LENGTH];
            var destChunk = new byte[SOURCE_BUFFER_LENGTH];

            var decodeBuffer = stackalloc byte[BASE32_QUANTUM_LENGTH];
            var decodeBufferLength = 0;
            var padLength = 0;

            var chunkFork = 1d / GetChunkCount(input.Length);

            var decodingState = DecodingState.ConsumingDigits;

            fixed (byte* fSourceChunk = sourceChunk)
            fixed (byte* fDestChunk = destChunk)
            fixed (byte* decodeTable = DECODE_TABLE)
            {
                while (true)
                {
                    using (pt.Fork(chunkFork))
                    {
                        var pSourceChunk = fSourceChunk;
                        var pDestChunk = fDestChunk;

                        var outputLength = 0;

                        ct.ThrowIfCancellationRequested();

                        var chunkLength = input.Read(sourceChunk, 0, BASE32_BUFFER_LENGTH);
                        if (chunkLength <= 0) // end of input
                        {
                            // no pending quantum
                            if (decodeBufferLength == 0)
                                break;

                            // last quantum of input
                            if (decodeBufferLength + padLength == BASE32_QUANTUM_LENGTH)
                            {
                                var quantumLen = CommitQuantum(decodeBuffer, decodeBufferLength, pDestChunk);
                                outputLength += quantumLen;

                                output.Write(destChunk, 0, outputLength);

                                break;
                            }

                            // UNDER/OVERPADDING
                            if (decodeBufferLength + padLength != BASE32_QUANTUM_LENGTH)
                            {
                                throw new InvalidPaddingException();
                            }

                            // the end IS COMPLETELY UNEXPECTED
                            throw new UnexpectedEndOfStreamException();
                        }

                        ct.ThrowIfCancellationRequested();

                        // decoding chunk
                        while (chunkLength-- > 0)
                        {
                            var ch = *pSourceChunk++;

                            // skip CRLF
                            if (ch == CR || ch == LF) continue;

                            switch (decodingState)
                            {
                                case DecodingState.ConsumingDigits:

                                    if (ch == PAD)
                                    {
                                        // then begin consuming pads

                                        decodingState = DecodingState.ConsumingPads;
                                        goto case DecodingState.ConsumingPads;
                                    }

                                    var decoded = decodeTable[ch];
                                    if (decoded == INVALID_CHARACTER) throw new InvalidCharacterException(ch);

                                    // consuming digit
                                    decodeBuffer[decodeBufferLength++] = decoded;

                                    // if we consumed 8 digits, commiting them
                                    if (decodeBufferLength == BASE32_QUANTUM_LENGTH)
                                    {
                                        CommitQuantum(decodeBuffer, decodeBufferLength, pDestChunk);
                                        outputLength += SOURCE_QUANTUM_LENGTH;
                                        pDestChunk += SOURCE_QUANTUM_LENGTH;

                                        decodeBufferLength = 0;
                                    }

                                    break;

                                case DecodingState.ConsumingPads:

                                    if (ch == PAD)
                                    {
                                        padLength++;
                                    }
                                    else
                                    {
                                        // according to RFC, we MAY skip pads that is not in the end of input 

                                        padLength = 0;
                                        decodingState = DecodingState.ConsumingDigits;

                                        // then treat ch as digit
                                        goto case DecodingState.ConsumingDigits;
                                    }

                                    break;
                            }
                        }

                        ct.ThrowIfCancellationRequested();

                        output.Write(destChunk, 0, outputLength);

                        pt.Report(ProgressToken.MAX_VALUE);
                    }
                }

                pt.Report(ProgressToken.MAX_VALUE);
            }
        }
    }
}